/* Copyright (c) 2011-2013 ISP RAS, Moscow. For more information please write to

      mermakov@ispras.ru
      svartanov@ispras.ru

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package ru.ispras.coffeemachine.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.OptionDef;
import org.kohsuke.args4j.spi.Parameters;
import org.kohsuke.args4j.spi.Setter;
import org.kohsuke.args4j.spi.StringOptionHandler;

import ru.ispras.coffeemachine.instrument.Classfile;
import ru.ispras.coffeemachine.instrument.ClassfileSet;

/**
 * Coffee options parser
 *
 * @author Sergey Vartanov <me@enzet.ru>
 * @version 1.2.0
 * @since 2011-09-13
 */
public class Options
{
	public static class ClassfileOptionHandler extends StringOptionHandler
	{
		public ClassfileOptionHandler(CmdLineParser parser, OptionDef option, Setter<? super String> setter)
		{
			super(parser, option, setter);
		}

		@Override
		public int parseArguments(Parameters params) throws CmdLineException
		{
			try
			{
				String className = params.getParameter(0);
				if (className.startsWith("!"))
				{
					classfiles.add(new Classfile(className.substring(1), "rt.jar"));
				}
				else
				{
					classfiles.add(new Classfile(className, null));
				}
				return 1;
			}
			catch (IndexOutOfBoundsException e)
			{
				throw new CmdLineException(owner, "Invalid class file name");
			}
			catch (ClassNotFoundException e)
			{
				throw new CmdLineException(owner, e.getLocalizedMessage());
			}
		}
	}

	public static class ClassfileFileOptionHandler extends StringOptionHandler
	{
		public ClassfileFileOptionHandler(CmdLineParser parser, OptionDef option, Setter<? super String> setter)
		{
			super(parser, option, setter);
		}

		@Override
		public int parseArguments(Parameters params) throws CmdLineException
		{
			String file = params.getParameter(0);
			try
			{
				Options.addClassFiles(file);
				return 1;
			}
			catch (IndexOutOfBoundsException e)
			{
				throw new CmdLineException(owner, "invalid file name");
			}
			catch (IOException e)
			{
				throw new CmdLineException(owner, "cannot open class files list " + file);
			}
			catch (ClassNotFoundException e)
			{
				throw new CmdLineException(owner, e.getLocalizedMessage());
			}
		}
	}

	public static class InputOptionHandler extends StringOptionHandler
	{
		public InputOptionHandler(CmdLineParser parser, OptionDef option, Setter<? super String> setter)
		{
			super(parser, option, setter);
		}

		@Override
		public int parseArguments(Parameters params) throws CmdLineException
		{
			try
			{
				String input = params.getParameter(0);
				inputs.add(input);
				File file = new File(input);
				if (!file.exists()) { throw new CmdLineException(owner, "No such file: " + input); }
				inputLengths.add((int) file.length());
				return 1;
			}
			catch (IndexOutOfBoundsException e)
			{
				throw new CmdLineException(owner, "Invalid input file");
			}
		}
	}

	public static class ExcludeListOptionHandler extends StringOptionHandler
	{

		boolean strict;
		public ExcludeListOptionHandler(CmdLineParser parser, OptionDef option,
										Setter<? super String> setter)
		{
			super(parser, option, setter);
			try {
				if (option.metaVar().contains("strict")) {
					strict = true;
				}
			} catch (Exception e) {

			}
		}

		@Override
		public int parseArguments(Parameters params) throws CmdLineException
		{
			try
			{
				String input = params.getParameter(0);
				if (strict) {
					strictExcludeList = getList(input);
				} else {
					excludeList = getList(input);
				}
				return 1;
			} catch (Exception e) {
				throw new CmdLineException(owner, "Cannot parse exclusion list");
			}
		}

	}

	public static class JAROptionHandler extends StringOptionHandler
	{
		public JAROptionHandler(CmdLineParser parser, OptionDef option, Setter<? super String> setter)
		{
			super(parser, option, setter);
		}

		@Override
		public int parseArguments(Parameters params) throws CmdLineException
		{
			String jarName = params.getParameter(0);

			try
			{
				setter.addValue(jarName);
				JarInputStream jar = new JarInputStream(new FileInputStream(jarName));
				JarEntry jarEntry;

				while ((jarEntry = jar.getNextJarEntry()) != null)
				{
					if (jarEntry.getName().contains("coffeemachine")) {
						continue;
					}
					if (jarEntry.getName().endsWith(".class"))
					{
						String className = jarEntry.getName();

						if (className != null)
						{
							className = className.replace(".class", "");
						}
						if (Options.prefix != null) {
							if (!className.startsWith(Options.prefix)) {
								Logger.info("Skipping " + className + " as item with different prefix");
								continue;
							}
						}
						if (Options.excludeList != null) {
							boolean skip = false;
							for (String excludeClass : excludeList)
							{
								if (className.contains(excludeClass)) {
									Logger.info("Skipping " + className + " as item of exclude list");
									skip = true;
									break;
								}
							}
							if (skip) {
								continue;
							}
						}
						if (Options.strictExcludeList != null) {
							boolean skip = false;
							for (String excludeClass : strictExcludeList)
							{
								if (className.equals(excludeClass)) {
									Logger.info("Skipping " + className + " as item of strict exclude list");
									skip = true;
									break;
								}
							}
							if (skip) {
								continue;
							}
						}
						try {
							classfiles.add(new Classfile(className, jarName));
						}
						catch (ClassNotFoundException e)
						{
							Logger.error(e.getLocalizedMessage());
						}
					}
				}
				jar.close();
			}
			catch (IOException e)
			{
				Logger.error("Error on " + jarName + ": " + e.getLocalizedMessage());
			}
			return 1;
		}
	}

	@Option(name = "-h", aliases = "--help", usage = "Print help banner")
	public static boolean printHelp = false;

	// Main classfile of main runnable JAR file

	@Option(name = "-tcf", usage = "Target classfile to be used as launching during execution (containing main(String []argv) method)", metaVar = "<classfile>")
	public static String targetClassfile;

	@Option(name = "-tj", usage = "Target runnable JAR file without path prefix", handler = JAROptionHandler.class, metaVar = "<JAR file>")
	public static String targetJar;

	// Classfiles of JAR files for instrumentation

	@Option(name = "-cf", handler = ClassfileOptionHandler.class, aliases = "--classfile", usage = "Additional classfile to be instrumented", metaVar = "<classfile>")
	public static String classFile;

	@Option(name = "-j", aliases = "--jar", handler = JAROptionHandler.class, usage = "Full path to JAR file to be instrument", metaVar = "<JAR file>")
	public static String jar;

	@Option(name = "--instrument", usage = "Perform instrumentation but do not run analysis")
	public static boolean instrumentOnly = false;

	@Option(name = "-iid", aliases = "--instrument-id", usage = "Use specified instrumentation ID instead generated", metaVar = "<number>")
	public static long instrumentationID = -1;

	@Option(name = "-pq", aliases = "--queries", usage = "Print generated STP queries")
	public static boolean printQueries = false;

	@Option(name = "-po", aliases = "--print-output", usage = "Print executed processes output")
	public static boolean printExec = false;

	@Option(name = "-sr", aliases = "--single-run")
	public static boolean doSingleRun = false;

	@Option(name = "-t", aliases = "--timestamps", usage = "Add timestamps to log messages")
	public static boolean timeStamps = false;

	@Option(name = "-mc", aliases = "--methods-coverage", usage = "Print concurrency methods coverage after instrumentation")
	public static boolean printMethodsCoverage = false;

	@Option(name = "-pi", aliases = "--print-instructions", usage = "Print information about invoked instructions")
	public static boolean printInstructions = false;

	@Option(name = "-sd", aliases = "--standard-daemon", usage = "Add STP instrumentation")
	public static boolean stpEnabled = false;

	@Option(name = "--keep-temp", usage = "Keep intermediate files")
	public static boolean keepTemp = false;

	@Option(name = "-dir", aliases = "--instrumentation-dir", usage = "Name of directory for instrumented files", metaVar = "<name>")
	public static String instrumentationDir;

	@Option(name = "-tdir", aliases = "--temp-dir", usage = "Name of temp directory", metaVar = "<name>")
	public static String tempDir;

	@Option(name = "-tp", aliases = "--trace-prefix", usage = "Instrumentation prefix", metaVar = "<prefix>")
	public static String tracePrefix;

	@Option(name = "-rt", aliases = "--rt-jar-path", usage = "Path to rt.jar", metaVar = "<rt.jar>")
	public static String rtPath;

	@Option(name = "-ifc", usage = "Instruction factory type (Console as default)", metaVar = "[Console|android.Android]")
	public static String instructionFactory = "Console";

	@Option(name = "-stp", usage = "Path to STP executable", metaVar = "<path to STP>")
	public static String stp = "stp";

	@Option(name = "-prefix", usage = "Class name prefix filter")
	public static String prefix;

	@Option(name = "--exclude-list", handler = ExcludeListOptionHandler.class, metaVar = "<exclude-list>")
	public static List<String> excludeList;

	@Option(name = "--strict-exclude-list", handler = ExcludeListOptionHandler.class, metaVar = "<strict-exclude-list>")
	public static List<String> strictExcludeList;

	@Option(name = "-bin", usage = "Binary directory of Coffee Machine", metaVar = "<directory>")
	public static String binDir;

	@Option(name = "-e", aliases = "--events", usage = "Events for ThreadSanitizer Offline file name", metaVar = "<file>")
	public static String eventsFileName = "events";

	@Option(name = "-ce", aliases = "--concurrency-events", usage = "File path on target device to store TSan trace", metaVar = "<path>")
	public static String concurrencyEventsFile = null;

	@Option(name = "-cc", aliases = "--concurrency", usage = "Enable concurrency defects detection")
	public static boolean concurrencyEnabled = false;

	@Option(name = "-mt", aliases = "--multithreading", usage = "Enable multithreading mode")
	public static boolean isMultithreading;

	@Option(name = "-cpf", aliases = "--code-pos-file", usage = "Path to code position list", metaVar = "<path>")
	public static String codePositionFile = "code-positions";

	/** All classfiles including java system classfiles needed to execute */
	public static ClassfileSet classfiles;

	/** List of classfiles form rt.jar we want to instrument */
	@Option(name = "-CF", handler = ClassfileFileOptionHandler.class, usage = "List of classfiles from rt.jar that will be instrumented", metaVar = "<file>")
	public static String classFilesList;

	/** Input files */
	@Option(name = "-i", handler = InputOptionHandler.class, aliases = "--input", usage = "Target application input file", metaVar = "<input file>")
	public static List<String> inputs;

	public static List<Integer> inputLengths;

	/** Command line arguments for application */
	@Argument
	public static List<String> arguments = new ArrayList<String>();

	public Options() throws IOException, InterruptedException
	{
		classfiles = new ClassfileSet();
		inputs = new ArrayList<String>();
		inputLengths = new ArrayList<Integer>();

		long number = new Date().getTime();

		tempDir = "coffee_temp_" + number + "/";
		instrumentationDir = "coffee_instrument_" + number + "/";
	}

	/**
	 * Add class files from list
	 *
	 * @param fileName name of file with list of class files
	 */
	static void addClassFiles(String fileName) throws IOException, ClassNotFoundException
	{
		FileReader f = new FileReader(fileName);
		BufferedReader br = new BufferedReader(f);

		String line;

		String className = "";
		String method = "";
		String jar = null;

		while ((line = br.readLine()) != null)
		{
			if (line.charAt(0) != ' ')
			{
				className = line.trim();

				if (className.endsWith(".class"))
				{
					className = className.substring(0, className.length() - ".class".length());
				}
				if (className.contains("."))
				{
					className = className.replaceAll("\\.", "/");
				}
				if (className.startsWith("!"))
				{
					className = className.substring(1);
					jar = "rt.jar";
				}
			}
			else
			{
				method = line;

				if (method.trim().equals("#")) // all methods in class
				{
					classfiles.add(new Classfile(className, jar));
				}
				else
				{
					Classfile classfile = classfiles.find(className);
					if (classfile == null)
					{
						Set<String> methods = new HashSet<String>();
						methods.add(method.trim());
						classfiles.add(new Classfile(className, jar, methods));
					}
					else
					{
						classfile.addMethod(method.trim());
					}
				}
			}
		}
		br.close();
	}

	static List<String> getList(String input) throws Exception
	{
		input = input.replaceAll(" ", "");
		List<String> result = new ArrayList<String>();
		if (input.contains(","))
		{
			String []classNames = input.split(",");
			for (String className : classNames)
			{
				result.add(className);
			}
		} else {
			result.add(input);
		}

		return result;
	}

	/**
	 * Detect any option problems and generate instrumentation ID
	 */
	public static void check() throws Exception
	{
		if (!printHelp)
		{
			if (((targetClassfile == null) && (targetJar == null)) ||
					((targetJar != null) && (targetClassfile != null)))
			{
				throw new Exception("you should specify target classfile or target JAR file");
			}
			if (concurrencyEnabled && !isMultithreading)
			{
				throw new Exception("you should specify multithreading mode for concurrency defects detection");
			}
			if ((targetJar == null) && (rtPath != null))
			{
				targetJar = "rt.jar";
			}
		}

		if (instrumentationID != -1)
		{
			long number = instrumentationID;
			tempDir = "coffee_temp_" + number + "/";
			instrumentationDir = "coffee_instrument_" + number + "/";
		}
	}
}
